﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Dynamic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using HtmlAgilityPack;
using Newtonsoft.Json.Linq;
using Noria.Web.MyMSDN.Models;
using Noria.Web.Utility;
using MmContext = Noria.Web.MyMSDN.Models.MmContext;

namespace Noria.Web.MyMSDN.Workers
{
    internal class ChecksumWorker : IWorker
    {
        private static readonly log4net.ILog slog = log4net.LogManager.GetLogger("Noria.Spider");
        private static readonly log4net.ILog alog = log4net.LogManager.GetLogger("Noria.Audit");

        private static readonly string rootChecksum = (string)NConfig.Root.spider.settings.checksum;
        private static readonly string urlChecksum = rootChecksum + "/getProduct.aspx";

        private readonly IStatistics stat;
        private readonly AsyncWaiter waiter;

        private dynamic root;

        private ChecksumWorker(IStatistics stat, AsyncWaiter waiter)
        {
            this.stat = stat;
            this.waiter = waiter;
            root = new ExpandoObject();
        }

        public static IWorker Create(IStatistics stat, AsyncWaiter waiter)
        {
            return new ChecksumWorker(stat, waiter);
        }

        public void Work()
        {
            if (waiter.IsAlive)
                GetCategoryFamily();

            if (waiter.IsAlive)
                GetFile();

            if (!waiter.IsAlive)
                stat.Cancel();
        }

        internal void GetFile(bool test = false)
        {
            if (test)
            {
                // win7
                root.Families = new[] { new ChecksumFamily { ProductId = "hGiRyJdHs5M=" } };
            }

            Parallel.ForEach((IEnumerable<ChecksumFamily>)root.Families, (family, state) =>
                {
                    // check live
                    if (!waiter.IsAlive)
                        state.Break();

                    // think time
                    Thread.Sleep(200);

                    string body = string.Format("Action=post&productsCategoryID={0}",
                        HttpUtility.UrlEncode(family.ProductId));

                    dynamic payload;

                    try
                    {
                        payload = HttpBot.RequestJObject(stat, urlChecksum, Encoding.UTF8.GetBytes(body), "application/x-www-form-urlencoded");
                    }
                    catch (Exception e)
                    {
                        slog.Error("Failed to retrieve checksum from family " + family.ProductId, e);
                        stat.AddError();
                        return;
                    }

                    // convert
                    family.Checksum = new List<Checksum>();
                    foreach (var raw in (IEnumerable)payload.products)
                    {
                        try
                        {
                            dynamic file = JObject.Parse(raw.ToString());

                            var checksum = new Checksum()
                            {
                                FileName = (string)file.fileName,
                                Sha1 = (string)file.SHA1,
                                Size = (long)file.fileSize,
                                eMule = (string)file.Hash,

                                SubmittedDate = DateTime.Now,
                                Initiator = "itellyou"
                            };
                            family.Checksum.Add(checksum);
                        }
                        catch
                        {
                            slog.ErrorFormat("Invalid content {0}", raw);
                            stat.AddError();
                            return;
                        }
                    }

                    // log
                    slog.InfoFormat("Checksum File ({0}) retrieved, count={1}", family.ProductId, family.Checksum.Count);
                    foreach (var checksum in family.Checksum)
                    {
                        slog.DebugFormat("File -> Name: {0}, Size: {1}, eMule: {2}.",
                                         checksum.FileName, checksum.Size, checksum.eMule);
                    }
                });

            // diff with db, update if necessary
            using (var db = new MmContext())
            {
                // join all the checksum into one
                var checksum = new List<Checksum>();
                foreach (var family in (IEnumerable<ChecksumFamily>)root.Families)
                {
                    checksum.AddRange(family.Checksum ?? new List<Checksum>());
                }

                // get all db checksum
                //var dbChecksum = db.Checksum.ToArray();

                //// distinct by sha1 and size
                //checksum = checksum.DistinctBy(p => new { p.Sha1, p.Size }).ToList();

                //MergeChecksum(checksum, dbChecksum, db.Checksum);
                foreach (var cs in checksum)
                {
                    db.Checksum.Add(cs);
                }

                int ret = db.SaveChanges();
                alog.InfoFormat("Checksum updated, count={0}", ret);
            }
        }

        internal void GetCategoryFamily(bool test = false)
        {
            var content = HttpBot.RequestText(stat, rootChecksum);
            var doc = new HtmlDocument();
            doc.LoadHtml(content);

            var pidList = doc.DocumentNode.SelectNodes("//span[@pid]").
                Select(p => new ChecksumFamily { ProductId = p.Attributes[0].Value }).ToArray();

            root.Families = pidList;

            slog.InfoFormat("Checksum Family retrieved, count={0}", pidList.Length);
        }

        private void MergeChecksum(IEnumerable<Checksum> memory, IEnumerable<Checksum> db, DbSet<Checksum> context)
        {
            foreach (var file in memory)
            {
                var dbFile = db.FirstOrDefault(p => p.Sha1 == file.Sha1 && p.Size == file.Size);
                if (dbFile != null)
                {
                    bool updated = false;

                    if (dbFile.FileName != file.FileName)
                    {
                        // update
                        dbFile.FileName = file.FileName;
                        updated = true;
                    }

                    if (dbFile.eMule != file.eMule)
                    {
                        // update
                        dbFile.eMule = file.eMule;
                        updated = true;
                    }

                    if (updated)
                    {
                        dbFile.SubmittedDate = file.SubmittedDate;
                        dbFile.Initiator = file.Initiator;

                        stat.AddUpdated();
                    }
                }
                else
                {
                    // insert
                    context.Add(file);
                    stat.AddInserted();
                }
            }
        }

        private class ChecksumFamily
        {
            public string ProductId { get; set; }
            public ICollection<Checksum> Checksum { get; set; }
        }
    }


}